package pprg.core;

import java.util.Observable;
import java.util.Random;
import java.util.concurrent.CyclicBarrier;

/**
 * <b> GAME OF LIFE </b>
 * <p>
 * Das Spielfeld (World) besetht aus einer 2D Matrix. Jeder Eintrag dieser
 * Matrix kann entweder true (Zelle lebt) oder false (Zelle ist tot) sein. Das
 * Spiel ist iterationsbasiert. Das heitßt die nächste Generation lässt sich
 * stets aus der akutuellen ermitteln. Dabei werden folgende Regeln befolgt:
 * </p>
 * <ul>
 * <li>
 * Tote Zelle mit genau drei lebenden Nachbarn wird in der Folgegeneration neu
 * geobren.</li>
 * Lebende Zelle mit weniger als zwei lebenden Nachbarn sterben in der
 * Folgegeneration.
 * <li>
 * Lebende Zelle mit zwei oder drei Nachbarn bleibt in der Folgegeneration am
 * Leben.</li>
 * <li>
 * Lebende Zelle mit mehr als drei lebenden Nachbarn sterben in der
 * Folgegeneration.</li>
 * </ul>
 * 
 * @author Heily Daniel, Hinterleitner Michael
 *
 */
public class GameOfLife extends Observable {

	private int arraySize;
	World world;
	private int[][] sumArray;
	private Random r;

	GameOfLifeWorker[] workers;
	public boolean stopped;

	/**
	 * Create Game of Life instance.
	 * 
	 * @param arraySize
	 *            the size of the quadratic game world {@link World}.
	 * @param startPopulationChance
	 *            chance for each cell to life in first genartion.
	 */
	public GameOfLife(int arraySize, double startPopulationChance) {
		this.arraySize = arraySize;
		world = new World(0, 0, arraySize, arraySize);
		r = new Random();
		createRandomStartGeneration(startPopulationChance);
	}

	/**
	 * calculate one sequential iteration. One thread calculats and updates all
	 * cells of the game-world.
	 * 
	 * @param gui
	 *            if true, the visualization is informed and updated.
	 */
	public void sequentialIteration(boolean gui) {
		sumArray = new int[arraySize][arraySize];
		calcSumsSequential();
		updateWorld();
		if (gui) {
			setChanged();
			notifyObservers();
		}
	}

	/**
	 * Start parallel calculation for the game of life
	 * 
	 * @param numThreads
	 *            number of threads that should be used.
	 * @param gui
	 *            should GUI be informed or not
	 * @param iterations
	 *            number of iterations
	 */
	public void parallelGameOfLife(int numThreads, boolean gui, int iterations) {
		sumArray = new int[arraySize][arraySize];
		stopped = false;
		try {
			prallelCalculation(numThreads, gui, iterations);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private void calcSumsSequential() {
		for (int x = 0; x < arraySize; x++) {
			for (int y = 0; y < arraySize; y++) {
				sumArray[y][x] = sum(x, y);
			}
		}
	}

	/**
	 * Parallel Calculation for the game of life. numThreads are created. Each
	 * thread has a {@link GameOfLifeWorker} Runnable. This is responsible to
	 * calculate a defined amount of rows of the game-world. The single threads
	 * are synchronized with a {@link CyclicBarrier}. This means every thread
	 * calls, after finishing its work for this iteration (calculating sum of
	 * alive neighbours + updating cells) the await() method of the barrier.
	 * When all the threads finished their work, the barrier thread is exectued.
	 * This one takes the updated world-parts from the single worker threads and
	 * puts them together.
	 * 
	 * @param numThreads
	 *            number of threads
	 * @param gui
	 *            true if you want to update GUI
	 * @param iterations
	 *            number of iterations
	 * @throws InterruptedException
	 */
	private void prallelCalculation(int numThreads, boolean gui, int iterations)
			throws InterruptedException {

		int poolsize = numThreads;
		workers = new GameOfLifeWorker[poolsize];

		CyclicBarrier barrier = new CyclicBarrier(poolsize, new Runnable() {
			@Override
			public void run() {
				boolean[][] updatedBoard = new boolean[arraySize][arraySize];
				for (GameOfLifeWorker w : workers) {
					// get updated board from worker and copy them all together
					boolean[][] subBoard = w.getWorldBoard();
					for (int i = w.getY(); i < w.getY() + w.getNumRows(); i++) {
						System.arraycopy(subBoard[i - w.getY()], 0,
								updatedBoard[i], 0, arraySize);
					}
					w.incrementIteration();
					// check if iterations are reached -> notify observer
					// (performance test) with endTime.
					if (w.getIterationCount() == iterations) {
						long endTime = System.currentTimeMillis();
						setChanged();
						notifyObservers(endTime);
					}
				}
				world.setBoard(updatedBoard);

				// give updated boards back to workers (upper and bottom row may
				// have changed in last iteration)
				for (GameOfLifeWorker w : workers) {
					World sw = world.getSubWorld(w.getX(), w.getY() - 1, w
							.getWorld().getWidth(),
							w.getWorld().getHeight() );
					w.setWorld(sw);

				}
				// check if calculation was stopped by GUI
				if (stopped) {
					for (GameOfLifeWorker w : workers) {
						w.stop();
					}
				}
				// if gui --> notify the visualisation to render new content
				if (gui) {
					setChanged();
					notifyObservers();
				}
			}
		});

		// create the worker threads: Each threads gets about the same amount of
		// rows on which it has to work.

		int yStep = world.getHeight() / poolsize;
		int rest = world.getHeight() % poolsize;

		for (int i = 0; i < poolsize; i++) {
			int numRows = yStep;
			if (i == poolsize - 1) {
				numRows = rest + yStep;
			}
			GameOfLifeWorker job = new GameOfLifeWorker(0, yStep * i, numRows,
					world.getSubWorld(0, yStep * i - 1, world.getWidth(),
							numRows + 2), iterations, barrier);
			workers[i] = job;
			new Thread(job).start();
		}

	}

	private int sum(int i, int j) {
		int sum = 0;
		for (Direction dir : Direction.values()) {
			if (world.isAlive(i + dir.dx, j + dir.dy)) {
				sum++;
			}
		}
		return sum;
	}

	private void updateWorld() {
		for (int x = 0; x < arraySize; x++) {
			for (int y = 0; y < arraySize; y++) {
				if (world.isAlive(x, y)) {
					if (sumArray[y][x] < 2 || sumArray[y][x] > 3) {
						world.setAlive(false, x, y);
					}
				} else if (sumArray[y][x] == 3) {
					world.setAlive(true, x, y);
				}
			}
		}
	}

	/**
	 * Create random population
	 * 
	 * @param chance
	 *            chance for each cell to life.
	 */
	public void createRandomStartGeneration(double chance) {
		for (int i = 0; i < arraySize; i++) {
			for (int j = 0; j < arraySize; j++) {
				if (r.nextDouble() <= chance) {
					world.setAlive(true, i, j);
				}
			}
		}
		setChanged();
		notifyObservers();
	}

	public boolean[][] getArray() {
		return world.getBoard();
	}

	public void setArray(boolean[][] array) {
		world.setBoard(array);
	}

	public void stopParallelCalculation() {
		stopped = true;
	}

}
